﻿using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.IO;
using System.Timers;
using System.IO.Compression;

public class SocketAdapter
{
		public static SocketAdapter Instance;

		// Adapter Configure
		public static bool Debug;

		// Adapter Beat Flag
		public byte[] beat = Encoding.UTF8.GetBytes ("b");

		// CallBack Adapter
		private Socket connectSocket;
		public InstanceData<Socket> connectOpen;
		public InstanceData<bool> connectClose;
		private bool logined;
		public InstanceData<bool, string> connectLogin;
		protected LinkedList<CallBack> loginedCallBackQueue = new LinkedList<CallBack> ();

		// CallBack String
		public CallBackDatas<int, byte[]> connectCallBacks;
		private int callBackIndex;
		private Dictionary<int, CallBackDatas<int, byte[]>> connectCallBacksDic = new Dictionary<int, CallBackDatas<int, byte[]>> ();

		// Beat Time Out
		public int beatTimeout = 20000;
		private int beatNextTime = MeContext.getContextTick();
		private bool beatReceived;
		private object reconnectFlag;
		protected static float Reconnect_Time = 30;
		protected const int TIME_OUT_MAX = 60000;
		private LinkedList<InstanceData<bool, int>> timeoutCallBackList = new LinkedList<InstanceData<bool, int>> ();

		// RECEIVE BUFFER
		// protected CallBackData<int> receiveStream;
		protected const byte DEBUG_FLAG = 0x01 << 7;
		protected const byte CALLBACK_FLAG = 0x01 << 6;
		protected const byte POST_FLAG = 0x01 << 5;
		protected const int RECEIVE_BUFFER_SIZE = 1024;
		protected byte[] ReceiveBuffer = new byte[RECEIVE_BUFFER_SIZE];

		public SocketAdapter ()
		{
				MeContext.addContextCallBack ((contextTick) => {
						return step (contextTick);
				});
		}

		public Socket getClient ()
		{
				return connectSocket;
		}

		public bool isLogined ()
		{
				return logined;
		}

		public void setCallBacks (int index, CallBackDatas<int, byte[]> callBacks)
		{
				if (connectCallBacksDic.ContainsKey (index)) {
						connectCallBacksDic.Remove (index);
				}
		
				connectCallBacksDic.Add (index, callBacks);
		}

		public Socket connect ()
		{
				Socket socket = connectSocket;
				if (socket == null) {
						socket = connectOpen ();
						if (socket != null) {
								connectSocket = socket;
								//Debug.Log ("connect logined = false");
								logined = false;
								beatReceived = true;
								//socket.BeginReceive (ReceiveBuffer, 0, RECEIVE_BUFFER_SIZE, SocketFlags.None, new AsyncCallback (receiveCallBack), socket);
						}
				}

				return socket;
		}

		public void connectSend (Socket socket, byte[] url, byte[] postData)
		{
				byte[] sendBytes = sendDataBytes (url, false, Debug, 0, postData);
				socket.BeginReceive (ReceiveBuffer, 0, RECEIVE_BUFFER_SIZE, SocketFlags.None, receiveCallBack, socket);
				socket.BeginSend (sendBytes, 0, sendBytes.Length, SocketFlags.None, sendCallBack, socket);
		}

		private const int CALLBACK_MIN = 255;

		public void close (bool reconnect)
		{
				//UnityEngine.Debug.Log ("colse-Reason" + new Exception ());
				if (connectSocket != null) {
						connectSocket.Close ();
						connectSocket = null;
						//UnityEngine.Debug.Log ("close logined = false");
						logined = false;
						loginedCallBackQueue.Clear ();
						foreach (InstanceData<bool, int> timeoutCallBack in timeoutCallBackList) {
								timeoutCallBack (-1);
						}

						List<KeyValuePair<int, CallBackDatas<int, byte[]>>> pairs = new List<KeyValuePair<int, CallBackDatas<int, byte[]>>> ();
						foreach (KeyValuePair<int, CallBackDatas<int, byte[]>> pair in connectCallBacksDic) {
								if (pair.Key < CALLBACK_MIN) {
										pairs.Add (pair);
								}
						}

						connectCallBacksDic.Clear ();
						foreach (KeyValuePair<int, CallBackDatas<int, byte[]>> pair in connectCallBacksDic) {
								connectCallBacksDic.Add (pair.Key, pair.Value);
						}
						
						timeoutCallBackList.Clear ();
						if (reconnect || connectClose ()) {
								//Debug.Log("reconnect.............");
								reconnectFlag = new object ();
								object flag = reconnectFlag;
								MeContext.addCastEnumerator (GameMonoUtils.getDelayEnumerator (Reconnect_Time, () => {
										//Debug.Log ("reconnect......flag" + flag + " reconnectFlag" + reconnectFlag);					
										if (flag == reconnectFlag) {
												connect ();
										}
								})
								);
						}
				}
		}

		// Receive Data
		protected int receiveLength;
		protected int receiveDataLength;
		protected byte[] receiveDataBytes;

		protected virtual void receiveCallBack (IAsyncResult ar)
		{
				//Debug.Log("SocketAdapter:=====receiveCallBack" + ar.AsyncState);
				if (ar.IsCompleted) {
						int len = connectSocket.EndReceive (ar);
				
						try {
								for (int i = 0; i < len; i++) {
										byte bit = ReceiveBuffer [i];
										if (receiveLength < 4) {
												receiveDataLength += (bit << (receiveLength * 8));
												if (++receiveLength == 4) {
														if (receiveDataLength > 0) {
																//UnityEngine.Debug.Log ("SocketAdapter:===============receiveDataLength" + receiveDataLength);
																receiveDataBytes = new byte[receiveDataLength];
								
														} else {
																receiveLength = 0;
														}
												}
										} else if (receiveDataLength > 0) {
												receiveDataBytes [receiveDataBytes.Length - receiveDataLength] = bit;
												if (--receiveDataLength <= 0) {
														if (receiveDataBytes.Length > 0) {
																//Debug.Log("*********addCallBack==========" + " socket...." + Encoding.UTF8.GetString(receiveDataBytes, 0, receiveDataBytes.Length));
																byte[] castReceiveDataBytes = receiveDataBytes;
																MeContext.addCastCallBack (() => {
																		//Debug.Log("=========receiveCallBackData==========" + " socket...." + Encoding.UTF8.GetString(receiveDataBytes, 0, receiveDataBytes.Length));
																		receiveCallBackData (castReceiveDataBytes);});
														}
							
														receiveLength = 0;
														receiveDataLength = 0;
												}
										} 
								}
				
						} finally {
								connectSocket.BeginReceive (ReceiveBuffer, 0, RECEIVE_BUFFER_SIZE, SocketFlags.None, receiveCallBack, connectSocket);
						}

				} else {
						// receive failed
						MeContext.addCastCallBack (() => {
								close (false);});
				}
		}

		protected virtual void receiveCallBackData (byte[] receiveDataBytes)
		{
				//Debug.Log("=================receiveDataBytes["  + receiveDataBytes.Length +  "] . "+ connectSocket + receiveDataBytes[0]);
				if (connectSocket == null) {
						return;
				}

				if (logined) {
						if (CommonUtils.compare (beat, receiveDataBytes)) {
								receiveBeat ();

						} else {
								int callBackIndex = 0;
								int dataLength = receiveDataBytes.Length;
								int headLength = 0;
								//string receiveData = null;
								if (dataLength >= 5) {
										int flag = receiveDataBytes [0];
										//success = (flag & DEBUG_FLAG) == 0;
										if ((flag & (CALLBACK_FLAG)) != 0) {
												callBackIndex += receiveDataBytes [1];
												callBackIndex += receiveDataBytes [2] << 8;
												callBackIndex += receiveDataBytes [3] << 16;
												callBackIndex += receiveDataBytes [4] << 24;
												headLength = 5;
												//receiveData = Encoding.UTF8.GetString (receiveDataBytes, 5, dataLength - 5);
						
										} else {
												headLength = 1;
												//receiveData = Encoding.UTF8.GetString (receiveDataBytes, 1, dataLength - 1);
										}
										
										if ((flag & DEBUG_FLAG) != 0) {
												headLength = - headLength;
										}
								}

								//MemoryStream inStream = new MemoryStream (receiveDataBytes, headLength, dataLength - headLength);
								//string receiveData;
								//if (Debug) {
								//receiveData = Encoding.UTF8.GetString (receiveDataBytes, headLength, dataLength - headLength);
								//
								//} else {
								//receiveDataBytes = CommonUtils.zipDeCompress (new MemoryStream (receiveDataBytes, headLength, dataLength - headLength));
								//receiveData = Encoding.UTF8.GetString (receiveDataBytes, 0, receiveDataBytes.Length);
								//}

								receiveCallBackData (callBackIndex, headLength, receiveDataBytes);
						}

				} else {
						if (connectLogin (Encoding.UTF8.GetString (receiveDataBytes, 0, receiveDataBytes.Length))) {
								logined = true;
								beatReceived = true;
								foreach (CallBack loginCallBack in loginedCallBackQueue) {
										loginCallBack ();
								}

								loginedCallBackQueue.Clear ();
								/*
								LinkedListNode<CallBack> loginedCallBackNode;
								while ((loginedCallBackNode = loginedCallBackQueue.First) != null) {
										loginedCallBackNode.Value ();
										loginedCallBackQueue.RemoveFirst ();
								}
								*/
						}
				}
		}

		protected virtual void receiveCallBackData (int callBackIndex, int pos, byte[] receiveDataBytes)
		{
				if (callBackIndex == 0) {
						connectCallBacks (pos, receiveDataBytes);
			
				} else {
			//UnityEngine.Debug.Log("  receiveCallBackData===: "+NetUtils.getReceiveValue<string>(receiveDataBytes,pos)+" callBackIndex:"+callBackIndex);
						CallBackDatas<int, byte[]> callBackDatas = null;
						connectCallBacksDic.TryGetValue (callBackIndex, out callBackDatas);
						if (callBackDatas != null) {
								if (callBackIndex > CALLBACK_MIN) {
										connectCallBacksDic.Remove (callBackIndex);
								}

								callBackDatas (pos, receiveDataBytes);
						}
				}
		}

		// HEAD DEBUG|CALLBACKINDEX|POST
		protected virtual byte[] sendDataBytes (byte[] dataBytes, bool head, bool debug, int callBackIndex, byte[] postData)
		{
				byte headFlag = 0x00;
				int headLength = callBackIndex == 0 ? 4 : 8;
				if (head) {
						headLength++;
				} else if (callBackIndex != 0) {
						head = true;
						headLength++;
				}
		
				int dataLength = dataBytes.Length;
				byte[] sendDataBytes;
				if (postData == null) {
						// no post
						dataLength += headLength;
						sendDataBytes = new byte[dataLength];
						Array.Copy (dataBytes, 0, sendDataBytes, headLength, dataLength - headLength);
			
				} else {
						// post head
						if (!head) {
								head = true;
								headLength++;
						}

						headFlag |= POST_FLAG;
						headLength += 4;
						int postLength = postData.Length;
						dataLength += headLength + postLength;
						sendDataBytes = new byte[dataLength];
						Array.Copy (dataBytes, 0, sendDataBytes, headLength, dataLength - headLength - postLength);
						Array.Copy (postData, 0, sendDataBytes, dataLength - postLength, postLength);
						sendDataBytes [headLength - 4] = (byte)postLength;
						sendDataBytes [headLength - 3] = (byte)(postLength >> 8);
						sendDataBytes [headLength - 2] = (byte)(postLength >> 16);
						sendDataBytes [headLength - 1] = (byte)(postLength >> 24);
				}
		
				// headFlag
				if (head) {
						if (debug) {
								headFlag |= DEBUG_FLAG;
						}
			
						if (callBackIndex != 0) {
								headFlag |= CALLBACK_FLAG;
								sendDataBytes [5] = (byte)callBackIndex;
								sendDataBytes [6] = (byte)(callBackIndex >> 8);
								sendDataBytes [7] = (byte)(callBackIndex >> 16);
								sendDataBytes [8] = (byte)(callBackIndex >> 24);
						}

						sendDataBytes [4] = headFlag;
				}
		
				// send data bytes length
				dataLength -= 4;
				sendDataBytes [0] = (byte)dataLength;
				sendDataBytes [1] = (byte)(dataLength >> 8);
				sendDataBytes [2] = (byte)(dataLength >> 16);
				sendDataBytes [3] = (byte)(dataLength >> 24);
				return sendDataBytes;
		}

		// step tick
		public virtual bool step (int contextTick)
		{
				//UnityEngine.Debug.Log("SocketAdapterTimeoutCallBackList" + timeoutCallBackList.Count);
				stepBeat (contextTick);
				LinkedListNode<InstanceData<bool, int>> timeoutCallBackNode = timeoutCallBackList.First;
				for (LinkedListNode<InstanceData<bool, int>> node = timeoutCallBackNode; node != null; node = timeoutCallBackNode) {
						timeoutCallBackNode = node.Next;
						if (node.Value (contextTick)) {
								timeoutCallBackList.Remove (node.Value);
						}
				}

				return false;
		}

		// step beat
		public void stepBeat (int contextTick)
		{
				//UnityEngine.Debug.Log("stepBeat......................" + beatNextTime + "::" + contextTick);
				if (beatNextTime < contextTick) {
						beatNextTime = contextTick + beatTimeout;
						//UnityEngine.Debug.Log("stepBeat......................" + connectSocket + "::" + beatReceived);
						if (beatReceived) {
								beatReceived = false;
								//Debug.Log("sendstepBeat:");
								send (beat, null, false, false, 0);

						} else {
								if (connectSocket != null) {
										close (false);
								}
						}
				}
		}
	
		protected void receiveBeat ()
		{
				//UnityEngine.Debug.Log("receiveBeatBeat......................" + connectSocket + "::" + beatReceived);
				beatReceived = true;
		}

		protected InstanceData<bool, int> generateTimeoutCallBack (int timeout, CallBack timeoutCallBack, int callBackIndex)
		{
				return (tick) => { 
						if (tick == -1 || timeout <= tick) { 
								if (connectCallBacksDic.ContainsKey (callBackIndex)) {
										connectCallBacksDic.Remove (callBackIndex);
										if (timeoutCallBack != null) {
												timeoutCallBack ();
										}
								}
								;
				
								return true; 
						}
			
						return false;
				};
		}

		public void send (byte[] urlData, byte[] postData, bool head, bool debug, int callBackIndex)
		{
				Socket socket = connect ();
				if (socket == null) {
						return;
				}

				CallBack callBack = () => {
						postData = sendDataBytes (urlData, head, debug, callBackIndex, postData);
						try {
								//socket.Send (postData, 0, postData.Length, SocketFlags.None);
								socket.BeginSend (postData, 0, postData.Length, SocketFlags.None, sendCallBack, socket);
					
						} catch (Exception e) {
								UnityEngine.Debug.Log ("SocketAdapterSendException:" + e);
								MeContext.addCastCallBack (() => {
										if (socket == connectSocket) {
												close (false);
										}});
						}
				};

				//UnityEngine.Debug.Log("SocketAdapterSend:" + urlData + "=>logined:" + logined);
				if (logined) {
						callBack ();
			
				} else {
						loginedCallBackQueue.AddLast (callBack);
				}
		}

		protected void sendCallBack (IAsyncResult ar)
		{
				//UnityEngine.Debug.Log("SocketAdapterSendCallBack" + ar + "=>completed" + ar.IsCompleted);
				if (!ar.IsCompleted) {
						MeContext.addCastCallBack (() => {
								if (ar.AsyncState == connectSocket) {
										close (false);
								}
						});
				}
		}

		public void send (byte[] urlData, byte[] postData, int timeout, CallBack timeoutCallBack, CallBackDatas<int, byte[]> callBacks)
		{
				int callBackIndex = generateCallBackIndex ();
				connectCallBacksDic.Add (callBackIndex, callBacks);
				send (urlData, postData, true, Debug, callBackIndex);
				if (timeout <= 0) {
						timeout = TIME_OUT_MAX;
				}
		
				timeoutCallBackList.AddLast (generateTimeoutCallBack (Environment.TickCount + timeout, timeoutCallBack, callBackIndex));
		}
	
		private int gCallBackIndex = CALLBACK_MIN;
	
		protected virtual int generateCallBackIndex ()
		{
				while (true) {
						if (!connectCallBacksDic.ContainsKey (++gCallBackIndex)) {
								return gCallBackIndex;
						}
			
						if (gCallBackIndex >= Int32.MaxValue) {
								gCallBackIndex = CALLBACK_MIN;
						}
				}
		
				return 0;
		}
}
